const assert = require('node:assert');
const { rollup } = require('../../dist/rollup');
const { loader } = require('../utils.js');

function runTestCode(code, thisValue, globals) {
  const globalThisDesc = Object.getOwnPropertyDescriptor(global, 'globalThis');
  delete global.globalThis;

  const globalsWithAssert = { ...globals, assert };
  const globalKeys = Object.keys(globalsWithAssert);
  const function_ = new Function(globalKeys, code);
  function_.apply(
    thisValue,
    globalKeys.map(key => globalsWithAssert[key])
  );
  if (globalThisDesc) {
    Object.defineProperty(global, 'globalThis', globalThisDesc);
  }
}

function runNodeTest(code) {
  const module = { exports: {} };
  runTestCode(code, module.exports, {
    module,
    exports: module.exports,
    require: importee => {
      if (importee !== 'external') {
        throw new Error(`Tried to import unknown "${importee}".`);
      }
      return runNodeTest('module.exports = "external";');
    }
  });
  return module.exports;
}

function runAmdTest(code) {
  let defineArguments;
  function define(...parameters) {
    defineArguments = parameters;
  }
  define.amd = true;

  runTestCode(code, undefined, { define });
  if (typeof defineArguments[0] === 'function') {
    return defineArguments[0]() || {};
  }
  const module = { exports: {} };
  const result =
    defineArguments[1](
      ...defineArguments[0].map(injection => {
        switch (injection) {
          case 'module': {
            return module;
          }
          case 'exports': {
            return module.exports;
          }
          case 'external': {
            return 'external';
          }
          default: {
            throw new Error(`unexpected AMD injection ${injection}`);
          }
        }
      })
    ) || {};
  return defineArguments[0].includes('exports') ? module.exports : result;
}

function runTestWithThis(code, outputOptions) {
  const global = { external: 'external' };
  runTestCode(code, global, {});
  return getAndCheckIifeExports(global, outputOptions);
}

function runStrictIifeTestWithSelf(code, outputOptions) {
  const global = { external: 'external' };
  runTestCode('"use strict";' + code, undefined, { self: global });
  return getAndCheckIifeExports(global, outputOptions);
}

function runStrictIifeTestWithGlobalThis(code, outputOptions) {
  const global = { external: 'external' };
  runTestCode('"use strict";' + code, undefined, { globalThis: global });
  return getAndCheckIifeExports(global, outputOptions);
}

function runIifeWithExistingValuesTest(code, outputOptions) {
  const global = {
    external: 'external',
    bundle: 'previous',
    my: { '@nested/value': { bundle: 'previous' } }
  };
  runTestCode(code, global, {});
  const exports = getAndCheckIifeExports(global, outputOptions);
  if (outputOptions.noConflict) {
    // As "noConflict" was already called when getting the exports, the previous globals should be restored
    assert.deepEqual(
      global,
      { external: 'external', bundle: 'previous', my: { '@nested/value': { bundle: 'previous' } } },
      'noConflict restores global'
    );
  }
  return exports === 'previous' ? {} : exports;
}

function getAndCheckIifeExports(global, outputOptions) {
  const exports = getIifeExports(global, outputOptions);
  if (outputOptions.noConflict) {
    assert.deepEqual(exports.noConflict(), exports, 'noConflict() returns exports');
    delete exports.noConflict;
  }
  return exports;
}

function getIifeExports(global, outputOptions) {
  if (outputOptions.name) {
    return outputOptions.name
      .split('.')
      .reduce((currentVariable, nextKey) => currentVariable[nextKey] || {}, global);
  }
  return {};
}

async function getUmdCode(inputCode, outputOptions) {
  const bundle = await rollup({
    input: 'input',
    external: ['external'],
    plugins: [loader({ input: inputCode })]
  });
  const { output } = await bundle.generate({
    format: 'umd',
    globals: { external: 'external' },
    ...outputOptions
  });
  return output[0].code;
}

function runTestsWithCode(code, outputOptions, expectedExports) {
  let umdCodePromise;

  function getUmdCodePromise() {
    if (umdCodePromise) {
      return umdCodePromise;
    }
    return (umdCodePromise = getUmdCode(code, outputOptions));
  }

  for (const { environmentName, runTest } of [
    {
      environmentName: 'node',
      runTest: runNodeTest
    },
    {
      environmentName: 'amd',
      runTest: runAmdTest
    },
    {
      environmentName: 'iife with this',
      runTest: runIifeTestWithThis
    },
    {
      environmentName: 'strict mode iife with self',
      runTest: runStrictIifeTestWithSelf
    },
    {
      environmentName: 'strict mode iife with globalThis',
      runTest: runStrictIifeTestWithGlobalThis
    },
    {
      environmentName: 'iife with existing globals',
      runTest: runIifeWithExistingValuesTest
    }
  ])
    it(`works in ${environmentName} environment`, async () => {
      assert.deepEqual(
        runTest(await getUmdCodePromise(), outputOptions),
        expectedExports,
        'expected exports are returned'
      );
    });
}

for (const name of ['bundle', 'my.@nested/value.bundle'])
  for (const compact of [false, true]) {
    for (const noConflict of [false, true])
      describe(`The UMD wrapper with name="${name}", compact=${compact}, noConflict=${noConflict}`, () => {
        const outputOptions = { compact, name, noConflict };

        describe('creating a bundle with neither exports nor imports', () =>
          runTestsWithCode('assert.ok(true);', outputOptions, {}));

        describe('creating a bundle with named exports', () =>
          runTestsWithCode('export const x = 42;', outputOptions, { x: 42 }));

        describe('creating a bundle with a default export', () =>
          runTestsWithCode('export default {value: 42};', outputOptions, { value: 42 }));

        describe('creating a bundle with an external import', () =>
          runTestsWithCode(
            'import value from "external"; assert.equal(value, "external");',
            outputOptions,
            {}
          ));

        describe('creating a bundle with an external import and named exports', () =>
          runTestsWithCode('import value from "external"; export const x = value;', outputOptions, {
            x: 'external'
          }));

        describe('creating a bundle with an external import and a default export', () =>
          runTestsWithCode('import value from "external"; export default {value};', outputOptions, {
            value: 'external'
          }));
      });
  }
